﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Xml;
using XmlStudio.Exceptions;
using XmlStudio.Interfaces;
using XmlStudio.Utilities;
using System.ComponentModel;
using System.Threading;

namespace XmlStudio.DataLayer {
    /// <summary>
    /// Reader built on top of XmlReader which makes it possible to read
    /// huge xml files without loading them into memory all at once. 
    /// </summary>
    internal class BigXmlReader : IBigXmlReader {
        #region Fields
        #region Private
        private string path;
        private bool opened;
        private XmlReader reader;
        private Address currentAddress = new Address();
        private XmlDocument document = new XmlDocument();
        private XmlReaderSettings xmlReaderSettings;
        public const int NodeStep = 5000;
        public const int Sleep = 5;
        #endregion
        #endregion

        #region Constructors
        #region Public
        /// <summary>
        /// Initializes a new instance of the <see cref="BigXmlReader"/> class.
        /// </summary>
        public BigXmlReader() {
        }

        /// <summary>
        /// Initializes a new instance of the BigXmlReader.
        /// </summary>
        /// <param name="path">Path to use.</param>
        /// <exception cref="System.ArgumentException">When path is null or empty.</exception>
        public BigXmlReader(string path) {
            //Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(path), "path");
            //Contract.Ensures(this.path == path);

            this.path = path;
            var info = new FileInfo(this.path);
            if(!info.Exists) {
                throw new FileNotFoundException(this.path);
            }

            var settings = new XmlReaderSettings { IgnoreWhitespace = true, DtdProcessing = DtdProcessing.Parse };

            this.xmlReaderSettings = settings;
        }
        #endregion
        #endregion

        #region Properties
        #region Public
        /// <summary>
        /// Gets the <see cref="XmlDocument"/> to create nodes.
        /// </summary>
        public XmlDocument XmlDocument {
            get { return this.document; }
        }

        /// <summary>
        /// Gets the value indicating whether the reader is opened.
        /// </summary>
        public bool Opened {
            get { return this.opened; }
        }

        /// <summary>
        /// Gets the file's path.
        /// </summary>
        public string Path {
            get { return this.path; }
            set { this.path = value; }
        }

        public int TotalNodeCount {
            get;
            private set;
        }

        /// <summary>
        /// Gets the reader's current address.
        /// </summary>
        public Address CurrentAddress {
            get { return this.currentAddress; }
        }
        #endregion
        #endregion

        #region Methods
        #region Public
        /// <summary>
        /// Checks document's well formedness.
        /// </summary>
        /// <exception cref="System.Xml.XmlException">When document is not well formed.</exception>
        public void CheckWellFormedness() {
            //Contract.Ensures(this.Opened == false);

            this.Open();

            try {
                while(this.reader.Read()) {
                }
            } finally {
                this.Close();
            }
        }

        /// <summary>
        /// Gets the root element of the document.
        /// </summary>
        /// <exception cref="XmlStudio.Exceptions.BadBigXmlReaderStateException">When reader is opened before the method call.</exception>
        /// <exception cref="System.Xml.XmlException">When error occurs while reading the file.</exception>
        /// <returns>Root element as a BigXmlNode instance.</returns>
        public BigXmlNodeInternal GetRootElement() {
            //Contract.Ensures(this.Opened == false);

            if(!string.IsNullOrEmpty(this.Path)) {
                this.TotalNodeCount = this.CountNodes();
                this.Open();

                BigXmlNodeInternal root = null;
                while(this.reader.Read()) {
                    if(this.reader.NodeType == XmlNodeType.Element) {
                        UpdateAddress();
                        root = CreateElement(this.reader);
                        int c = GetChildCount(this.reader);
                        for(int i = 0; i < c; ++i) {
                            root.ChildNodes.Add(null);
                        }

                        break;
                    }
                }

                this.Close();

                return root;
            }

            return null;
        }

        /// <summary>
        /// Gets the child nodes of the element at the specified address.
        /// </summary>
        /// <param name="address">Address of the element.</param>
        /// <param name="range">Range of child nodes to get.</param>
        /// <returns>Ordered collection of child nodes of a certain element.</returns>
        /// <exception cref="System.ArgumentException">When range has bad format.</exception>
        public IEnumerable<BigXmlNodeInternal> GetChildNodes(int[] address, string range) {
            //Contract.Ensures(this.Opened == false);

            int childCount = this.GetChildCount(address);

            IEnumerable<int> parsedRange = null;
            if(!string.IsNullOrEmpty(range)) {
                parsedRange = range.ParseRange();
            } else {
                parsedRange = Enumerable.Range(1, childCount).ToList();
            }

            return this.GetChildNodes(address, parsedRange, childCount);
        }

        /// <summary>
        /// Gets the list of child nodes of the element specified in the address filtered by
        /// the range.
        /// </summary>
        /// <param name="address">Address of parent element.</param>
        /// <param name="range">Range to get.</param>
        /// <param name="childCount">The child count.</param>
        /// <returns>Collection of XmlNodes.</returns>
        /// <exception cref="System.ArgumentException">When address contains negative numbers or range is empty.</exception>
        public IEnumerable<BigXmlNodeInternal> GetChildNodes(int[] address, IEnumerable<int> range, int childCount) {
            var count = range.Count();

            var checkCount = count != 0;

            //var childCount = this.GetChildCount(address);

            var reader = this.NavigateToAddress(address);

            var moveReader = true;
            var read = reader.Read();
            var updateAddress = true;

            var childNodes = new List<BigXmlNodeInternal>();
            for(int i = 0; i < childCount; ++i) {
                childNodes.Add(null);
            }

            var currentDepth = reader.Depth;
            var currentCount = 1;
            var inserted = 0;

            try {
                while(read) {
                    if(moveReader) {
                        read = reader.Read();
                    } else {
                        moveReader = true;
                    }

                    switch(reader.NodeType) {
                        case XmlNodeType.Element:
                        case XmlNodeType.Text: {
                                BigXmlNodeInternal node = null;
                                if(updateAddress) {
                                    this.UpdateAddress();
                                } else {
                                    updateAddress = true;
                                }

                                if(range.Contains(currentCount)) {
                                    switch(reader.NodeType) {
                                        case XmlNodeType.Element:
                                            node = CreateElement(reader);
                                            childNodes[currentCount - 1] = node;
                                            inserted++;
                                            var c = GetChildCount(reader);
                                            for(var i = 0; i < c; ++i) {
                                                node.ChildNodes.Add(null);
                                            }

                                            break;
                                        case XmlNodeType.Text:
                                            node = CreateTextNode(reader);
                                            childNodes[currentCount - 1] = node;
                                            inserted++;
                                            break;
                                    }
                                } else {
                                    reader.Skip();
                                    moveReader = false;
                                }

                                currentCount++;

                                if(checkCount && inserted >= count) {
                                    return childNodes;
                                }

                                break;
                            }
                        case XmlNodeType.EndElement: {
                                if(reader.Depth == currentDepth) {
                                    return childNodes;
                                }
                                break;
                            }
                    }

                }
            } finally {
                this.Close();
            }

            return childNodes;
        }

        private int CountNodes() {
            this.Open();
            int nodes = 0;
            while(this.reader.Read()) {
                switch(this.reader.NodeType) {
                    case XmlNodeType.Element:
                        nodes++;
                        break;
                    case XmlNodeType.Text:
                        nodes++;
                        break;
                }
            }

            this.Close();
            return nodes;
        }

        /// <summary>
        /// Counts nodes at the specified subtree.
        /// </summary>
        /// <param name="address">Address of the root of the subtree.</param>
        /// <returns>Number of nodes.</returns>
        public int CountNodes(int[] address) {
            var r = this.NavigateToAddress(address);

            int nodes = 0;
            while(r.Read()) {
                switch(this.reader.NodeType) {
                    case XmlNodeType.Element:
                        nodes++;
                        break;
                    case XmlNodeType.Text:
                        nodes++;
                        break;
                }
            }

            this.Close();
            return nodes;
        }

        /// <summary>
        /// Writes the child nodes from the specified address to the specified <see cref="XmlWriter"/>.
        /// </summary>
        /// <param name="address">Address where to start.</param>
        /// <param name="writer"><see cref="XmlWriter"/> to use.</param>
        /// <param name="bw">The background worker..</param>
        /// <param name="range">The range.</param>
        /// <param name="writeRoot">if set to <c>true</c> [write root].</param>
        public void WriteChildNodesFromAddress(int[] address, XmlWriter writer, BackgroundWorker bw, IEnumerable<int> range, bool writeRoot = false) {
            this.NavigateToAddress(address);

            // get to the element
            while(this.reader.NodeType != XmlNodeType.Element) {
                this.reader.Read();
            }

            if(writeRoot) {
                BigXmlNodeInternal.NodeCount++;
                if(bw != null) {
                    if(BigXmlNodeInternal.NodeCount % NodeStep == 0) {
                        var progress = ((double)BigXmlNodeInternal.NodeCount / (double)BigXmlNodeInternal.Total) * 100;
                        bw.ReportProgress((int)progress);
                        Thread.Sleep(BigXmlReader.Sleep);
                    }
                }

                var prefix = writer.LookupPrefix(this.reader.NamespaceURI);
                if(!string.IsNullOrEmpty(prefix) && prefix == this.reader.Prefix) {
                    writer.WriteStartElement(this.reader.LocalName, this.reader.NamespaceURI);
                    if(this.reader.IsEmptyElement) {
                        writer.WriteEndElement();
                        writeRoot = false; // trick so that the end element won't be written the second time...
                    }
                } else {
                    writer.WriteStartElement(this.reader.Prefix, this.reader.LocalName, this.reader.NamespaceURI);
                }

                if(reader.HasAttributes) {
                    reader.MoveToFirstAttribute();

                    do {
                        writer.WriteAttributeString(this.reader.LocalName, this.reader.NamespaceURI, this.reader.Value);
                    } while(reader.MoveToNextAttribute());

                    reader.MoveToElement();
                }
            }

            int order = 0;
            var startDepth = this.reader.Depth;
            bool moveReader = false;
            bool read = this.reader.Read();
            //while(this.reader.Read() && this.reader.Depth > startDepth) {
            while (read && this.reader.Depth > startDepth) {
                if(moveReader) {
                    read = this.reader.Read();
                } else {
                    moveReader = true;
                }

                // get to the element
                while(this.reader.NodeType != XmlNodeType.Element && this.reader.NodeType != XmlNodeType.Text && this.reader.NodeType != XmlNodeType.EndElement && !this.reader.EOF) {
                    this.reader.Read();
                }

                if(this.reader.Depth <= startDepth || this.reader.EOF) {
                    break;
                }

                switch(this.reader.NodeType) {
                    case XmlNodeType.Element:
                        if(reader.Depth == startDepth + 1) {
                            order++;
                        }

                        if((reader.Depth == startDepth + 1 && range.Contains(order)) || reader.Depth != startDepth + 1) {
                            BigXmlNodeInternal.NodeCount++;
                            if(bw != null) {
                                if(BigXmlNodeInternal.NodeCount % NodeStep == 0) {
                                    var progress = ((double)BigXmlNodeInternal.NodeCount / (double)BigXmlNodeInternal.Total) * 100;
                                    bw.ReportProgress((int)progress);
                                    Thread.Sleep(BigXmlReader.Sleep);
                                }
                            }

                            var prefix = writer.LookupPrefix(this.reader.NamespaceURI);


                            if(!string.IsNullOrEmpty(prefix) && prefix == this.reader.Prefix) {
                                writer.WriteStartElement(this.reader.LocalName, this.reader.NamespaceURI);
                                if(this.reader.IsEmptyElement) {
                                    writer.WriteEndElement();
                                }
                            } else {
                                writer.WriteStartElement(this.reader.Prefix, this.reader.LocalName, this.reader.NamespaceURI);

                            }

                            if(reader.HasAttributes) {
                                reader.MoveToFirstAttribute();

                                do {
                                    writer.WriteAttributeString(this.reader.LocalName, this.reader.NamespaceURI, this.reader.Value);
                                } while(reader.MoveToNextAttribute());

                                reader.MoveToElement();
                            }

                            if(this.reader.IsEmptyElement) {
                                writer.WriteEndElement();
                            }
                        } else {
                            reader.Skip();
                            moveReader = false;
                        }
                        break;
                    case XmlNodeType.EndElement:
                        if((reader.Depth == startDepth + 1 && range.Contains(order)) || reader.Depth != startDepth + 1) {
                            writer.WriteEndElement();
                        }
                        break;
                    case XmlNodeType.Text:
                        if(reader.Depth == startDepth + 1) {
                            order++;
                        }

                        if((reader.Depth == startDepth + 1 && range.Contains(order)) || reader.Depth != startDepth + 1) {
                            BigXmlNodeInternal.NodeCount++;
                            if(bw != null) {
                                if(BigXmlNodeInternal.NodeCount % NodeStep == 0) {
                                    var progress = ((double)BigXmlNodeInternal.NodeCount / (double)BigXmlNodeInternal.Total) * 100;
                                    bw.ReportProgress((int)progress);
                                    Thread.Sleep(BigXmlReader.Sleep);
                                }
                            }

                            writer.WriteValue(this.reader.Value);
                        }
                        break;
                }
            }

            if(writeRoot) {
                writer.WriteEndElement();
            }

            this.Close();
        }

        /// <summary>
        /// Writes the child nodes from the specified address to the specified <see cref="XmlWriter"/>.
        /// </summary>
        /// <param name="address">Address where to start.</param>
        /// <param name="writer"><see cref="XmlWriter"/> to use.</param>
        /// <param name="bw">The background worker.</param>
        /// <param name="writeRoot">Whether to write the root of the addressed subtree.</param>
        public void WriteChildNodesFromAddress(int[] address, XmlWriter writer, BackgroundWorker bw, bool writeRoot = false) {
            this.NavigateToAddress(address);

            // get to the element
            while(this.reader.NodeType != XmlNodeType.Element) {
                this.reader.Read();
            }

            if(writeRoot) {
                BigXmlNodeInternal.NodeCount++;
                if(bw != null) {
                    if(BigXmlNodeInternal.NodeCount % NodeStep == 0) {
                        var progress = ((double)BigXmlNodeInternal.NodeCount / (double)BigXmlNodeInternal.Total) * 100;
                        bw.ReportProgress((int)progress);
                        Thread.Sleep(BigXmlReader.Sleep);
                    }
                }

                var prefix = writer.LookupPrefix(this.reader.NamespaceURI);
                if(!string.IsNullOrEmpty(prefix) && prefix == this.reader.Prefix) {
                    writer.WriteStartElement(this.reader.LocalName, this.reader.NamespaceURI);
                    if(this.reader.IsEmptyElement) {
                        writer.WriteEndElement();
                        writeRoot = false; // trick so that the end element won't be written the second time...
                    }
                } else {
                    writer.WriteStartElement(this.reader.Prefix, this.reader.LocalName, this.reader.NamespaceURI);
                }

                if(reader.HasAttributes) {
                    reader.MoveToFirstAttribute();

                    do {
                        writer.WriteAttributeString(this.reader.LocalName, this.reader.NamespaceURI, this.reader.Value);
                    } while(reader.MoveToNextAttribute());

                    reader.MoveToElement();
                }
            }

            var startDepth = this.reader.Depth;
            while(this.reader.Read() && this.reader.Depth > startDepth) {
                // get to the element
                while(this.reader.NodeType != XmlNodeType.Element && this.reader.NodeType != XmlNodeType.Text && this.reader.NodeType != XmlNodeType.EndElement && !this.reader.EOF) {
                    this.reader.Read();
                }

                if(this.reader.Depth <= startDepth || this.reader.EOF) {
                    break;
                }

                switch(this.reader.NodeType) {
                    case XmlNodeType.Element:
                        BigXmlNodeInternal.NodeCount++;
                        if(bw != null) {
                            if(BigXmlNodeInternal.NodeCount % NodeStep == 0) {
                                var progress = ((double)BigXmlNodeInternal.NodeCount / (double)BigXmlNodeInternal.Total) * 100;
                                bw.ReportProgress((int)progress);
                                Thread.Sleep(BigXmlReader.Sleep);
                            }
                        }

                        var prefix = writer.LookupPrefix(this.reader.NamespaceURI);


                        if(!string.IsNullOrEmpty(prefix) && prefix == this.reader.Prefix) {
                            writer.WriteStartElement(this.reader.LocalName, this.reader.NamespaceURI);
                            if(this.reader.IsEmptyElement) {
                                writer.WriteEndElement();
                            }
                        } else {
                            writer.WriteStartElement(this.reader.Prefix, this.reader.LocalName, this.reader.NamespaceURI);

                        }

                        if(reader.HasAttributes) {
                            reader.MoveToFirstAttribute();

                            do {
                                writer.WriteAttributeString(this.reader.LocalName, this.reader.NamespaceURI, this.reader.Value);
                            } while(reader.MoveToNextAttribute());

                            reader.MoveToElement();
                        }

                        if(this.reader.IsEmptyElement) {
                            writer.WriteEndElement();
                        }

                        break;
                    case XmlNodeType.EndElement:
                        writer.WriteEndElement();
                        break;
                    case XmlNodeType.Text:
                        BigXmlNodeInternal.NodeCount++;
                        if(bw != null) {
                            if(BigXmlNodeInternal.NodeCount % NodeStep == 0) {
                                var progress = ((double)BigXmlNodeInternal.NodeCount / (double)BigXmlNodeInternal.Total) * 100;
                                bw.ReportProgress((int)progress);
                                Thread.Sleep(BigXmlReader.Sleep);
                            }
                        }

                        writer.WriteValue(this.reader.Value);
                        break;
                }
            }

            if(writeRoot) {
                writer.WriteEndElement();
            }

            this.Close();
        }

        /// <summary>
        /// Gets the child count of the node specified in the address.
        /// </summary>
        /// <param name="address">Address of the parent node.</param>
        /// <returns>Number of child nodes.</returns>
        /// <exception cref="System.Xml.XmlException">When there is error with xml processing.</exception>
        /// <exception cref="System.ArgumentException">When address contains negative numbers.</exception>
        public int GetChildCount(int[] address) {
            //Contract.Ensures(this.Opened == false);

            int childCount = 0;
            try {
                var reader = this.NavigateToAddress(address);
                childCount = this.GetChildCount(reader);
            } finally {
                this.Close();
            }

            return childCount;
        }

        /// <summary>
        /// Navigates to a specified address and retrieves the node at that address with all its descendants. Basically reads
        /// the subtree from the document with the root at the specified address.
        /// </summary>
        /// <param name="address">Address in document where to start.</param>
        /// <returns>Reference to the root of the read subtree.</returns>
        public BigXmlNodeInternal GetAllFromAddress(int[] address) {
            try {
                var reader = this.NavigateToAddress(address);

                // move on element
                while(reader.NodeType != XmlNodeType.Element) {
                    reader.Read();
                }

                XmlNode root = null;
                using(var subReader = reader.ReadSubtree()) {
                    root = XmlDocument.ReadNode(subReader);
                }

                var addr = address.ToList();

                var parsedRoot = this.ParseXmlNode(root, addr);

                return parsedRoot;
            } finally {
                this.Close();
            }
        }

        /// <summary>
        /// Opens reader from the path that was set in construction.
        /// </summary>
        /// <exception cref="System.ArgumentNullException">Argument error.</exception>
        /// <exception cref="System.IO.IOException">IO error.</exception>
        /// <exception cref="System.Security.SecurityException">Security error.</exception>
        /// <exception cref="System.UriFormatException">Error in path.</exception>
        /// <exception cref="System.ArgumentException">When path is null or empty.</exception>
        public void Open() {
            //Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(this.Path));
            //Contract.Ensures(!string.IsNullOrEmpty(this.Path));

            this.reader = XmlReader.Create(this.path, xmlReaderSettings);
            this.opened = true;
        }

        /// <summary>
        /// Closes the reader.
        /// </summary>
        public void Close() {
            //Contract.Ensures(this.reader == null);
            //Contract.Ensures(this.opened == false);

            if(this.reader != null) {
                this.reader.Close();
                this.opened = false;
            }

            this.reader = null;
        }

        /// <summary>
        /// Navigates to the specified address, so on the next reader.Read() you get the element at the specified address.
        /// </summary>
        /// <param name="address">Address to navigate to.</param>
        /// <exception cref="System.Xml.XmlException">When address point to non-existent place in the document.</exception>
        /// <remarks>Address must contain only positive numbers or zeros.</remarks>
        public XmlReader NavigateToAddress(int[] address) {
            foreach(var i in address) {
                if(i < 0) {
                    throw new ArgumentException("address");
                }
            }
            //Contract.Requires<ArgumentException>(Contract.ForAll(address, i => i >= 0));
            //Contract.Requires<ArgumentException>(this.Opened == false);
            //Contract.Ensures(address.SequenceEqual(this.currentAddress));

            this.Open();
            this.ResetAddress();
            var read = true;
            var moveReader = true;
            var updateAddress = true;

            var currentOrder = 1;

            while(read) {
                if(moveReader) {
                    read = this.reader.Read();
                } else {
                    moveReader = true;
                }

                switch(this.reader.NodeType) {
                    case XmlNodeType.Element:
                    case XmlNodeType.Text: {
                            if(updateAddress) {
                                this.UpdateAddress();
                            } else {
                                updateAddress = true;
                            }

                            // when not on the right child
                            if(currentOrder < address[this.reader.Depth]) {
                                // skip this child
                                this.reader.Skip();
                                moveReader = false; // don't move the reader (when reader is set to skip whitespace is would end up at the wrong place)
                                updateAddress = false;
                                currentOrder++;
                                this.UpdateAddress();

                                // continue the loop only if the reader is not on the last item of the address
                                if(this.reader.Depth < address.Length - 1) {
                                    continue;
                                }

                            }

                            // when the right node is found, exit
                            //if (this.reader.Depth == address.Length - 1 && address[this.reader.Depth] == currentOrder)
                            if(this.CurrentAddress.SequenceEqual(address)) {
                                return this.reader.ReadSubtree();
                            }

                            // if we found the right child on the current depth,
                            // reset order so we start with 1 on the next one
                            if(currentOrder > 1 && currentOrder == address[this.reader.Depth]) {
                                currentOrder = 1;
                            }
                        }
                        break;
                }
            }

            this.Close();
            throw new XmlException("Address is out of scope of this document.");
        }

        //public void WriteDoctype(XmlDocumentType documentType, XmlWriter writer) {
        //    //this.Open();

        //    //while(this.reader.Read()) {
        //    //    switch(this.reader.NodeType) {
        //    //        case XmlNodeType.DocumentType:
        //    //            writer.WriteDocType(this.reader.Name, this.reader.GetAttribute("PUBLIC"), this.reader.GetAttribute("SYSTEM"), this.reader.Value);
        //    //            this.Close();
        //    //            return;
        //    //    }
        //    //}

        //    //this.Close();

        //}

        /// <summary>
        /// Gets the document type of the document if exists.
        /// </summary>
        /// <returns><see cref="XmlDocumentType"/> for current document or null if there is no doc type.</returns>
        public XmlDocumentType GetDocumentType() {
            this.Open();

            try {
                while(this.reader.Read()) {
                    switch(this.reader.NodeType) {
                        case XmlNodeType.DocumentType:
                            var docType = this.XmlDocument.CreateDocumentType(this.reader.Name, this.reader.GetAttribute("PUBLIC"), this.reader.GetAttribute("SYSTEM"), this.reader.Value);
                            return docType;
                        case XmlNodeType.Element:
                            return null;
                    }
                }
            } finally {
                this.Close();
            }

            return null;
        }
        #endregion

        #region Private
        private BigXmlNodeInternal ParseXmlNode(XmlNode node, IList<int> address) {
            var n = BigXmlNodeInternal.Create(node, address.ToArray(), address.ToArray());

            var i = 1;
            foreach(XmlNode child in node.ChildNodes) {
                if(child.NodeType == XmlNodeType.Element || child.NodeType == XmlNodeType.Text) {
                    var parsedChildAddress = address.DeepClone();
                    parsedChildAddress.Add(i);

                    var parsedChild = this.ParseXmlNode(child, parsedChildAddress);
                    n.ChildNodes.Add(parsedChild);
                    i++;
                }
            }

            return n;
        }

        /// <summary>
        /// Gets the number of child elements of the element previously addressed by a call to NavigateToAddress.
        /// </summary>
        /// <param name="reader">Reader positioned at the element of which to count the child nodes.</param>
        /// <returns>Number of child elements.</returns>
        /// <exception cref="System.Xml.XmlException">When underlying reader throws this exception.</exception>
        /// <exception cref="System.ArgumentException">When reader is null.</exception>
        private int GetChildCount(XmlReader reader) {
            //Contract.Requires<ArgumentNullException>(reader != null, "reader");

            var count = 0;
            var moveReader = true;

            var read = true;
            if(reader.NodeType == XmlNodeType.None) {
                read = reader.Read(); // now we are on the parent
            }

            var currentDepth = reader.Depth;

            if(reader.IsEmptyElement) {
                return 0;
            }

            while(read) {
                if(moveReader) {
                    read = reader.Read();
                } else {
                    moveReader = true;
                }

                switch(reader.NodeType) {
                    case XmlNodeType.Element:
                    case XmlNodeType.Text: {
                            // this.UpdateAddress();

                            // do not skip on text nodes
                            if(reader.NodeType == XmlNodeType.Element && !reader.IsEmptyElement) {
                                reader.Skip();
                                moveReader = false;
                            }

                            count++;
                            break;
                        }
                    case XmlNodeType.EndElement: {
                            if(reader.Depth == currentDepth) {
                                return count;
                            }
                            break;
                        }
                }
            }

            return count;
        }

        /// <summary>
        /// Updates current address according to the current position of the XmlReader.
        /// </summary>
        /// <exception cref="System.ArgumentNullException">When reader is null.</exception>
        private void UpdateAddress() {
            //Contract.Requires<ArgumentNullException>(this.reader != null, "Reader can not be null.");

            var added = false;
            while(this.reader.Depth > this.currentAddress.Count - 1) {
                this.currentAddress.Add(1);
                added = true;
            }

            if(!added) {
                this.currentAddress[this.reader.Depth]++;
            }
        }

        /// <summary>
        /// Resets current address.
        /// </summary>
        private void ResetAddress() {
            //Contract.Requires<BadBigXmlReaderStateException>(this.currentAddress != null);

            this.currentAddress.Clear();
        }

        /// <summary>
        /// Creates a text node from the current position of <see cref="XmlReader"/>.
        /// </summary>
        /// <returns><see cref="BigXmlNode"/> with the text node.</returns>
        private BigXmlNodeInternal CreateTextNode(XmlReader reader) {
            //Contract.Requires<ArgumentNullException>(reader != null, "Reader cannot be null.");
            //Contract.Requires<ArgumentException>(reader.NodeType == XmlNodeType.Text, "Reader is not set on the text node.");

            var text = this.document.CreateTextNode(reader.Value);

            return BigXmlNodeInternal.Create(text, this.currentAddress.ToArray(), this.currentAddress.ToArray());
        }

        /// <summary>
        /// Creates an element from the current position of <see cref="XmlReader"/>.
        /// </summary>
        /// <returns><see cref="BigXmlNode"/> reference to the created element.</returns>
        /// <exception cref="System.ArgumentNullException">When reader is null.</exception>
        /// <exception cref="System.ArgumentException">When reader is not set on the element.</exception>
        private BigXmlNodeInternal CreateElement(XmlReader reader) {
            //Contract.Requires<ArgumentNullException>(reader != null, "Reader cannot be null.");
            //Contract.Requires<ArgumentException>(reader.NodeType == XmlNodeType.Element, "Reader is not set on the element.");
            //Contract.Ensures(reader.NodeType == XmlNodeType.Element);

            var element = this.document.CreateElement(reader.Prefix, reader.LocalName, reader.NamespaceURI);

            if(reader.HasAttributes) {
                reader.MoveToFirstAttribute();

                do {
                    if(reader.NodeType == XmlNodeType.Attribute) {
                        //element.SetAttribute(reader.Name, reader.NamespaceURI, reader.Value);
                        if(reader.Prefix == "xmlns" || reader.Name == "xmlns") {

                        } else {
                            //element.SetAttribute(reader.LocalName, reader.NamespaceURI, reader.Value);
                            var att = this.document.CreateAttribute(reader.Prefix, reader.LocalName, reader.NamespaceURI);
                            att.Value = reader.Value;
                            element.SetAttributeNode(att);
                        }
                    }
                } while(reader.MoveToNextAttribute());

                reader.MoveToElement();
            }

            return BigXmlNodeInternal.Create(element, this.currentAddress.ToArray(), this.currentAddress.ToArray());
        }
        #endregion

        #region Static
        #region Public
        ///// <summary>
        ///// Parses string with range definition.
        ///// </summary>
        ///// <param name="rangeString">String with range, e.g. 1-10,12-15,16</param>
        ///// <returns>Return IEnumerable with ints representing individual numbers in desired range.</returns>
        ///// <exception cref="System.ArgumentException">When rangeString has bad format.</exception>
        //public static IEnumerable<int> ParseRange(String rangeString) {
        //    var res = new List<int>();

        //    if(string.IsNullOrEmpty(rangeString)) {
        //        return res;
        //    }

        //    foreach(var s in rangeString.Split(',')) {
        //        //try and get the number
        //        int num;
        //        if(int.TryParse(s, out num)) {
        //            if(num > 0) {
        //                res.Add(num);
        //            } else {
        //                throw new ArgumentException("Wrong input string, only positive numbers are allowed.");
        //            }
        //        } else {
        //            // otherwise we might have a range
        //            // split on the range delimiter
        //            var subs = s.Split('-');
        //            int start = 0, end = 0;

        //            //now see if we can parse a start and end
        //            if(subs.Length > 1 && int.TryParse(subs[0], out start) && int.TryParse(subs[1], out end) && end > start) {
        //                if(start <= 0 || end <= 0) {
        //                    throw new ArgumentException("Wrong input string, only positive numbers are allowed.");
        //                }

        //                //create a range between the two values
        //                res.AddRange(Enumerable.Range(start, end - start + 1));
        //            } else if(end == start && end > 0) {
        //                res.Add(start);
        //            } else {
        //                throw new ArgumentException("Wrong input string.");
        //            }
        //        }
        //    }

        //    res.Sort();
        //    return res.Distinct();
        //}
        #endregion
        #endregion
        #endregion
    }
}